package com.belladati.tutorial; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Calendar; import java.util.Collections; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import javax.imageio.ImageIO; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import com.belladati.sdk.BellaDatiService; import com.belladati.sdk.auth.OAuthRequest; import com.belladati.sdk.dashboard.DashboardInfo; import com.belladati.sdk.dataset.AttributeValue; import com.belladati.sdk.exception.auth.AuthorizationException; import com.belladati.sdk.exception.interval.InvalidIntervalException; import com.belladati.sdk.filter.Filter; import com.belladati.sdk.filter.Filter.MultiValueFilter; import com.belladati.sdk.filter.FilterOperation; import com.belladati.sdk.filter.FilterValue; import com.belladati.sdk.intervals.AbsoluteInterval; import com.belladati.sdk.intervals.DateUnit; import com.belladati.sdk.intervals.Interval; import com.belladati.sdk.report.Report; import com.belladati.sdk.report.ReportInfo; import com.belladati.sdk.view.View; import com.belladati.sdk.view.ViewLoader; import com.belladati.sdk.view.ViewType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; /** * Handles incoming page requests from the end user's browser. Connects to * BellaDati to fetch data and passes it on to the frontend for rendering. * * @author Chris Hennigfeld */ @Controller public class TutorialController { /** * Provides access to a {@link BellaDatiService} instance, automatically * injected via Spring. */ @Autowired private ServiceManager manager; /** Hardcoded ID of the data set */ private static final String DATA_SET_ID = "18812"; /** Hardcoded ID of the attribute used to filter */ private static final String ATTRIBUTE_CODE = "L_PRODUCT"; /** Stores the predefined filters for each view */ private final Map<String, Set<Filter<?>>> predefinedFilters = Collections .synchronizedMap(new HashMap<String, Set<Filter<?>>>()); /** * Handles the root URL. Redirects to the login page or the report page * depending on whether the user is logged in. */ @RequestMapping("/") public ModelAndView initialUrl() throws InterruptedException, ExecutionException { if (manager.isLoggedIn()) { return showReportDashboardList(); } else { return new ModelAndView("login"); } } /** * Loads the list of reports and dashboards from BellaDati and injects them * into the frontend view to display. */ public ModelAndView showReportDashboardList() throws InterruptedException, ExecutionException { ModelAndView modelAndView = new ModelAndView("list"); // start a service for parallel execution of requests ExecutorService service = Executors.newCachedThreadPool(); // provide thread-independent access final BellaDatiService bdService = manager.getService(); // submit requests for reports and dashboards Future<List<ReportInfo>> reportFuture = service.submit(new Callable<List<ReportInfo>>() { @Override public List<ReportInfo> call() throws Exception { return bdService.getReportInfo().load().toList(); } }); Future<List<DashboardInfo>> dashboardFuture = service.submit(new Callable<List<DashboardInfo>>() { @Override public List<DashboardInfo> call() throws Exception { return bdService.getDashboardInfo().load().toList(); } }); // stop the service once the requests are done service.shutdown(); // then inject the responses into the view modelAndView.addObject("reports", reportFuture.get()); modelAndView.addObject("dashboards", dashboardFuture.get()); return modelAndView; } /** * Loads report contents from BellaDati and injects them into the frontend * view for rendering. * * @param reportId ID of the report to load */ @RequestMapping("/report/{id}") public ModelAndView showReport(@PathVariable("id") String reportId) { if (!manager.isLoggedIn()) { return new ModelAndView("redirect:/?redirectUrl=/report/" + reportId); } Report report = manager.getService().loadReport(reportId); List<AttributeValue> values = manager.getService().getAttributeValues(DATA_SET_ID, ATTRIBUTE_CODE).loadFirstTime() .toList(); // store the views' predefined filters for later use for (View view : report.getViews()) { predefinedFilters.put(view.getId(), view.getPredefinedFilters()); } ModelAndView modelAndView = new ModelAndView("report"); modelAndView.addObject("report", report); modelAndView.addObject("commonInterval", getCommonMonthInterval(report)); modelAndView.addObject("attributeValues", values); return modelAndView; } /** * Looks for a common month-based interval in the report's views. Views * without an interval or with an interval that's not month-based are * ignored. * * @param report the report to examine * @return the interval if all month-based views share the same interval, * <tt>null</tt> if there are different intervals or no views have * month-based intervals */ private Interval<DateUnit> getCommonMonthInterval(Report report) { Interval<DateUnit> commonInterval = null; for (View view : report.getViews()) { // check each view's interval Interval<DateUnit> dateInterval = view.getPredefinedDateInterval(); if (dateInterval != null && dateInterval.getIntervalUnit() == DateUnit.MONTH) { // if the view has an interval that's month-based if (commonInterval == null) { // if we haven't seen a month-based interval yet, note it commonInterval = dateInterval; } else if (!commonInterval.equals(dateInterval)) { // if we've seen a different month interval before, return return null; } } } return commonInterval; } /** * Loads dashboard contents from BellaDati and injects them into the * frontend view for rendering. * * @param dashboardId ID of the dashboard to load */ @RequestMapping("/dashboard/{id}") public ModelAndView showDashboard(@PathVariable("id") String dashboardId) { if (!manager.isLoggedIn()) { return new ModelAndView("redirect:/?redirectUrl=/dashboard/" + dashboardId); } ModelAndView modelAndView = new ModelAndView("dashboard"); modelAndView.addObject("dashboard", manager.getService().loadDashboard(dashboardId)); return modelAndView; } /** * Loads the thumbnail image for the dashboard with the given ID. * * @param id ID of the dashboard * @return the dashboard's thumbnail, or an empty array if no thumbnail is * found */ @RequestMapping(value = "/dashboard/{id}/thumbnail", produces = "image/png") @ResponseBody public byte[] getDashboardThumbnail(@PathVariable String id) { return doGetThumbnail(true, id); } /** * Loads the thumbnail image for the report with the given ID. * * @param id ID of the report * @return the report's thumbnail, or an empty array if no thumbnail is * found */ @RequestMapping(value = "/report/{id}/thumbnail", produces = "image/png") @ResponseBody public byte[] getReportThumbnail(@PathVariable String id) { return doGetThumbnail(false, id); } /** * Performs loading a thumbnail image for the given ID. * * @param isDashboard <tt>true</tt> to load a dashboard image, * <tt>false</tt> for a report * @param id ID of the dashboard or report * @return the thumbnail, or an empty array if no thumbnail is found */ private byte[] doGetThumbnail(boolean isDashboard, String id) { try { final BufferedImage thumbnail; if (isDashboard) { thumbnail = (BufferedImage) manager.getService().loadDashboardThumbnail(id); } else { thumbnail = (BufferedImage) manager.getService().loadReportThumbnail(id); } ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(thumbnail, "png", baos); baos.flush(); byte[] bytes = baos.toByteArray(); baos.close(); return bytes; } catch (IOException e) { return new byte[0]; } } /** * Loads and returns the content of a chart from BellaDati. * * @param chartId ID of the chart to load * @param intervalString an optional JSON string representing the interval * to set, containing "from" and "to" elements with "year" and * "month" each * @return the JSON content of the chart */ @RequestMapping("/chart/{id}") @ResponseBody public JsonNode viewContent(@PathVariable("id") String chartId, @RequestParam(value = "interval", required = false) String intervalString, @RequestParam(value = "filterValues", required = false) String filterString) throws IOException { ViewLoader loader = manager.getService().createViewLoader(chartId, ViewType.CHART); // always exclude items with a blank product name loader.addFilters(FilterOperation.NOT_NULL.createFilter(manager.getService(), DATA_SET_ID, ATTRIBUTE_CODE)); if (intervalString != null) { try { JsonNode interval = new ObjectMapper().readTree(intervalString); Calendar from = new GregorianCalendar(interval.get("from").get("year").asInt(), interval.get("from").get("month") .asInt() - 1, 1); Calendar to = new GregorianCalendar(interval.get("to").get("year").asInt(), interval.get("to").get("month") .asInt() - 1, 1); AbsoluteInterval<DateUnit> dateInterval = new AbsoluteInterval<DateUnit>(DateUnit.MONTH, from, to); // if all is successful, use the interval when loading the chart loader.setDateInterval(dateInterval); } catch (IOException e) {} catch (InvalidIntervalException e) {} } if (filterString != null) { try { ArrayNode interval = (ArrayNode) new ObjectMapper().readTree(filterString); if (interval.size() > 0) { MultiValueFilter filter = FilterOperation.IN.createFilter(manager.getService(), DATA_SET_ID, ATTRIBUTE_CODE); for (JsonNode value : interval) { filter.addValue(new FilterValue(value.asText())); } // if all is successful, // use the filter when loading the chart loader.addFilters(filter); } } catch (IOException e) {} } // and always include the predefined filter, if we have one if (predefinedFilters.containsKey(chartId)) { loader.addFilters(predefinedFilters.get(chartId)); } // load the chart return (JsonNode) loader.loadContent(); } /** * Redirects the user to BellaDati for OAuth authorization. */ @RequestMapping("/login") public ModelAndView redirectToAuth(@RequestParam(value = "redirectUrl", required = false) String redirectUrl, HttpServletRequest request) { String oauthRedirect = getDeploymentUrl(request) + "/authorize"; if (redirectUrl != null) { oauthRedirect += "?redirectUrl=" + redirectUrl; } OAuthRequest oAuthRequest = manager.initiateOAuth(oauthRedirect); return new ModelAndView("redirect:" + oAuthRequest.getAuthorizationUrl()); } /** * Landing page after OAuth authorization, reached by redirect from the * BellaDati server. Completes OAuth. */ @RequestMapping("/authorize") public ModelAndView requestAccessToken(@RequestParam(value = "redirectUrl", required = false) String redirectUrl, RedirectAttributes redirectAttributes) { try { manager.completeOAuth(); } catch (AuthorizationException e) { /* * show an error informing the user - use e.getReason() to show * custom error messages depending on what happened */ redirectAttributes.addFlashAttribute("error", "Authentication failed: " + e.getMessage()); } if (redirectUrl == null) { return new ModelAndView("redirect:/"); } else { return new ModelAndView("redirect:" + redirectUrl); } } /** * Logs out. */ @RequestMapping("/logout") public ModelAndView doLogout() { manager.logout(); return new ModelAndView("redirect:/"); } /** * Finds the root URL of the current deployment based on the user's request. * * @param request request from the user * @return the deployment root, including scheme, server, port, and path */ private String getDeploymentUrl(HttpServletRequest request) { String requestUrl = request.getRequestURL().toString(); String servletPath = request.getServletPath(); return requestUrl.substring(0, requestUrl.length() - servletPath.length()); } }